//------------------------------------------------------------------------------
// <copyright file="Updater.cs" company="Damian Antonowicz">
//       copyright © 2010 Damian Antonowicz
// </copyright>
// <author>Damian Antonowicz</author>
// <email>poczta@damianantonowicz.pl</email>
//------------------------------------------------------------------------------

using System;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Xml;
using System.ComponentModel;

namespace DAnton.Blocks.Updater
{
    public delegate void Action();

    public class ApplicationUpdaterBlock
    {
        #region private members
         
        SynchronizationContext synchCont;
        Action cancelMethod;

        HashAlgorithm hashAlgorithm;
        IUpdatesInfoProvider updateInfoProvider;
        IUpdatesDownloader updatesDownloader;

        Version newVersion,
                currentVersion;

        bool isBusy = false;

        string downloadUrl,
               programName,
               tempPath,
               updaterHash,
               updaterPath;

        #endregion

        #region properties

        public bool IsBusy
        {
            get { return this.isBusy; }
        }

        public event EventHandler<ProgressChangedEventArgs> CheckUpdatesProgressChanged;
        public event EventHandler<CheckUpdatesCompletedEventArgs> CheckUpdatesCompleted;

        public event EventHandler<DownloadUpdatesProgressChangedEventArgs> DownloadUpdatesProgressChanged
        {
            add { this.updatesDownloader.DownloadUpdatesProgressChanged += value; }
            remove { this.updatesDownloader.DownloadUpdatesProgressChanged -= value; }
        }

        public event EventHandler<DownloadUpdatesCompletedEventArgs> DownloadUpdatesCompleted;

        #endregion

        #region constructors

        public ApplicationUpdaterBlock(HashAlgorithm hashAlgorithm,
                                       IUpdatesDownloader updatesDownloader,
                                       IUpdatesInfoProvider updateInfoProvider)
        {
            if (hashAlgorithm == null)
                throw new ArgumentNullException("hashAlgorithm");

            if (updatesDownloader == null)
                throw new ArgumentNullException("updatesDownloader");

            if (updateInfoProvider == null)
                throw new ArgumentNullException("updateInfoProvider");

            this.hashAlgorithm = hashAlgorithm;
            this.updatesDownloader = updatesDownloader;
            this.updateInfoProvider = updateInfoProvider;

            this.synchCont = SynchronizationContext.Current;
        }

        #endregion

        #region public methods

        public void CancelAsync()
        {
            if (this.cancelMethod != null)
            {
                this.cancelMethod();
                this.cancelMethod = null;
            }

            this.isBusy = false;
        }

        public void CheckUpdatesAsync(Version currentVersion, string programName)
        {
            this.RaiseExceptionIfBusy();

            if (currentVersion == null)
                throw new ArgumentNullException("currentVersion");

            if (programName == null)
                throw new ArgumentNullException("programName");

            this.programName = programName;
            this.currentVersion = currentVersion;
            this.tempPath = Path.Combine(Path.GetTempPath(), String.Format("{0}_TempFolder", programName));
            this.isBusy = true;

            Thread thread = new Thread(new ThreadStart(this.CheckUpdates));
            thread.IsBackground = true;
            thread.Start();

            this.cancelMethod = () =>
                {
                    if (thread.IsAlive)
                    {
                        try
                        {
                            thread.Abort();
                        }
                        catch
                        {
                        }
                    }

                    this.OnEventOccured(this.CheckUpdatesCompleted, new CheckUpdatesCompletedEventArgs(null, true));
                };
        }

        public void DownloadUpdatesAsync()
        {
            this.RaiseExceptionIfBusy();
            this.isBusy = true;

            this.cancelMethod = () =>
                {
                    this.updatesDownloader.CancelAsync();
                };

            ThreadPool.QueueUserWorkItem((ob) => this.DownloadUpdates());
        }

        public void RunUpdater()
        {
            Process.Start(this.updaterPath, "\"" +  AppDomain.CurrentDomain.BaseDirectory + "\"");
        }

        #endregion

        #region private methods

        private void CheckUpdates()
        {
            try
            {
                this.OnEventOccured(this.CheckUpdatesProgressChanged, new ProgressChangedEventArgs(0, null));

                UpdateInfo updateInfo = this.updateInfoProvider.GetUpdateInfo();
                this.newVersion = updateInfo.LatestVersion;
                this.downloadUrl = updateInfo.UpdaterDownloadUrl;
                this.updaterHash = updateInfo.UpdaterHash;

                this.updaterPath = Path.Combine(this.tempPath, String.Format("{0}_{1}_updater.exe", this.programName, this.newVersion));
                this.OnEventOccured(this.CheckUpdatesProgressChanged, new ProgressChangedEventArgs(50, null));
                bool updatesAvailable = this.AreUpdatesAvailable();
                this.OnEventOccured(this.CheckUpdatesCompleted, new CheckUpdatesCompletedEventArgs(updatesAvailable, newVersion));
            }
            catch (Exception ex)
            {
                this.OnEventOccured(this.CheckUpdatesCompleted, new CheckUpdatesCompletedEventArgs(ex, false));
            }

            this.isBusy = false;
        }

        private bool AreUpdatesAvailable()
        {
            if (this.newVersion != null)
            {
                if (this.currentVersion.CompareTo(this.newVersion) < 0)
                    return true;
                else
                    return false;
            }
            else
                return false;
        }

        private void DownloadUpdates()
        {
            try
            {
                if (!this.IsUpdaterAlreadyDownloaded())
                {
                    this.CreateTempDir();
                    this.DeleteUpdaterIfExists();
                    this.DownloadUpdater();
                }
                else
                {
                    this.OnEventOccured(this.DownloadUpdatesCompleted, new DownloadUpdatesCompletedEventArgs(true));
                }
            }
            catch (Exception ex)
            {
                this.OnEventOccured(this.DownloadUpdatesCompleted, new DownloadUpdatesCompletedEventArgs(ex, false));
            }

            this.isBusy = false;
        }

        private bool IsUpdaterAlreadyDownloaded()
        {
            return this.updaterHash == this.GetDownloadedUpdaterHash();
        }

        private string GetDownloadedUpdaterHash()
        {
            if (File.Exists(this.updaterPath))
            {
                byte[] currentUpdaterHash = this.hashAlgorithm.ComputeHash(File.ReadAllBytes(this.updaterPath));
                return BitConverter.ToString(currentUpdaterHash).Replace("-", "");
            }
            else
                return String.Empty;
        }

        private void DownloadUpdater()
        {
            ManualResetEvent manualEvent = new ManualResetEvent(false);

            this.updatesDownloader.DownloadUpdatesCompleted += (object sender, AsyncCompletedEventArgs e) =>
                {
                    if (e.Cancelled || e.Error != null)
                        this.OnEventOccured(this.DownloadUpdatesCompleted, new DownloadUpdatesCompletedEventArgs(e.Error, e.Cancelled));
                    else
                        this.OnEventOccured(this.DownloadUpdatesCompleted, new DownloadUpdatesCompletedEventArgs(false));

                    manualEvent.Set();
                };

            this.updatesDownloader.DownloadUpdatesAsync(this.downloadUrl, this.updaterPath);
            manualEvent.WaitOne();
        }

        private void CreateTempDir()
        {
            if (!Directory.Exists(this.tempPath))
                Directory.CreateDirectory(this.tempPath);
        }

        private void DeleteUpdaterIfExists()
        {
            this.DeleteUpdaterIfExists(this.updaterPath, 0);
        }

        private void DeleteUpdaterIfExists(string filePath, int counter)
        {
            if (File.Exists(filePath))
            {
                try
                {
                    File.Delete(filePath);
                }
                catch
                {
                    counter++;
                    filePath = String.Format("{0}_#{1}", this.updaterPath, counter);
                    this.DeleteUpdaterIfExists(filePath, counter);
                }
            }

            if (counter > 0)
                this.updaterPath += String.Format("_#{0}", counter);
        }

        private void OnEventOccured<T>(EventHandler<T> handler, T args) where T : EventArgs
        {
            Action invokeMethod = () =>
            {
                if (handler != null)
                    handler(this, args);
            };

            if (this.synchCont != null)
            {
                this.synchCont.Post(new SendOrPostCallback((state) =>
                {
                    invokeMethod();
                }), null);
            }
            else
                invokeMethod();
        }

        private void OnEventOccured(EventHandler handler)
        {
            if (handler != null)
                handler(this, EventArgs.Empty);
        }

        private void RaiseExceptionIfBusy()
        {
            if (this.isBusy)
                throw new InvalidOperationException("class doesn't support multiple async operations");
        }

        #endregion
    }
}
